import websocket
import threading
import traceback
from time import sleep
import json
import logging
import urllib
import math
import pdb
import gzip
import time
from datetime import datetime
from urllib import parse
import requests
import okex.utils as utils 
import okex.consts as c 
import dateutil.parser as dp
import hmac
import base64

# Naive implementation of connecting to BitMEX websocket for streaming realtime data.
# The Marketmaker still interacts with this as if it were a REST Endpoint, but now it can get
# much more realtime data without polling the hell out of the API.
#
# The Websocket offers a bunch of data as raw properties right on the object.
# On connect, it synchronously asks for a push of all this data then returns.
# Right after, the MM can start using its data. It will be updated in realtime, so the MM can
# poll really often if it wants.
class Websocket:

    # Don't grow a table larger than this amount. Helps cap memory usage.
    MAX_TABLE_LEN = 200

    def __init__(self, endpoint, symbol, api_key=None, api_secret=None, passphrase=None):
        '''Connect to the websocket and initialize data stores.'''
        self.logger = logging.getLogger(__name__)
        #self.logger.setLevel(logging.DEBUG)  # Change this to DEBUG if you want a lot more info
        self.logger.debug("Initializing WebSocket.")

        self.endpoint = endpoint
        self.symbol = symbol

        if api_key is not None and api_secret is None:
            raise ValueError('api_secret is required if api_key is provided')
        if api_key is None and api_secret is not None:
            raise ValueError('api_key is required if api_secret is provided')

        self.api_key = api_key
        self.api_secret = api_secret
        self.passphrase = passphrase 

        self.data = {}
        self.keys = {}
        self.exited = False

        # We can subscribe right in the connection querystring, so let's build that.
        # Subscribe to all pertinent endpoints
        #WS-基础行情订阅,WSv1-用户信息订阅(需要鉴权),WSv2-基础非用户信息订阅
        self.wsURL_v2 = self.__get_url_v2()
        self.logger.info("Connecting to v2 %s" % self.wsURL_v2)
        self.wsURL = self.__get_url()
        self.logger.info("Connecting to %s" % self.wsURL)
        self.wsURL_v1 = self.__get_url_v1()
        self.logger.info("Connecting to v1 %s" % self.wsURL_v1)
        self.__connect_v2()
        self.logger.info('Connected to WS-v2.')
        self.__connect()
        self.logger.info('Connected to WS.')
        self.__connect_v1()
        self.logger.info('Connected to WS-v1.')

        # Connected. Wait for partials
        self.__wait_for_symbol(symbol)
        if api_key:
            self.__wait_for_account()
        self.logger.info('Got all market data. Starting.')



    def exit(self):
        '''Call this to exit - will close websocket.'''
        self.exited = True
        self.ws.close()

    def exit_v1(self):
        '''Call this to exit - will close websocket.'''
        self.exited = True
        self.ws_v1.close()

    def exit_v2(self):
        '''Call this to exit - will close websocket.'''
        self.exited = True
        self.ws_v2.close()

    def get_instrument(self):
        '''Get the raw instrument data for this symbol.'''
        # Turn the 'tickSize' into 'tickLog' for use in rounding
        instrument = self.data['instrument'][0]
        instrument['tickLog'] = int(math.fabs(math.log10(instrument['tickSize'])))
        return instrument

    def get_ticker(self):
        '''Return a ticker object. Generated from quote and trade.'''
        lastQuote = self.data['quote'][-1]
        lastTrade = self.data['trade'][-1]
        ticker = {
            "last": lastTrade['price'],
            "buy": lastQuote['bidPrice'],
            "sell": lastQuote['askPrice'],
            "mid": (float(lastQuote['bidPrice'] or 0) + float(lastQuote['askPrice'] or 0)) / 2
        }

        # The instrument has a tickSize. Use it to round values.
        instrument = self.data['instrument'][0]
        return {k: round(float(v or 0), instrument['tickLog']) for k, v in ticker.items()}

    def funds(self):
        '''Get your margin details.'''
        return self.data['margin'][0]

    def market_depth(self):
        '''Get market depth (orderbook). Returns all levels.'''
        return self.data['orderBookL2']

    def open_orders(self, clOrdIDPrefix):
        '''Get all your open orders.'''
        orders = self.data['order']
        # Filter to only open orders (leavesQty > 0) and those that we actually placed
        return [o for o in orders if str(o['clOrdID']).startswith(clOrdIDPrefix) and o['leavesQty'] > 0]

    def recent_trades(self):
        '''Get recent trades.'''
        return self.data['trade']

    #
    # End Public Methods
    #

    def __connect(self):

        '''Connect to the websocket in a thread.'''
        self.logger.debug("Starting thread")

        #websocket.enableTrace(True)
        self.ws = websocket.WebSocketApp(self.wsURL,
                                         on_message=self._on_message,
                                         on_open=self.__on_open,
                                         on_close=self.__on_close,
                                         on_error=self.__on_error)

        self.wst = threading.Thread(target=lambda: self.ws.run_forever())
        self.wst.daemon = True
        self.wst.start()
        self.logger.info("Started thread")

        # Wait for connect before continuing
        conn_timeout = 5
        while not self.ws.sock or not self.ws.sock.connected and conn_timeout:
            sleep(1)
            conn_timeout -= 1
        if not conn_timeout:
            self.logger.error("Couldn't connect to WS! Exiting.")
            self.exit()
            raise websocket.WebSocketTimeoutException('Couldn\'t connect to WS! Exiting.')
        
    #
    # End Public Methods
    #

    def __connect_v1(self):

        '''Connect to the websocket in a thread.'''
        self.logger.info("Starting thread-v1")

        #websocket.enableTrace(True)
        self.ws_v1 = websocket.WebSocketApp(self.wsURL_v1,
                                         on_message=self._on_message,
                                         on_open=self.__on_open_v1,
                                         on_close=self.__on_close_v1,
                                         on_error=self.__on_error_v1)

        self.wst_v1 = threading.Thread(target=lambda: self.ws_v1.run_forever())
        self.wst_v1.daemon = True
        self.wst_v1.start()
        self.logger.info("Started thread-v1")

        # Wait for connect before continuing
        conn_timeout_v1 = 5
        while not self.ws_v1.sock or not self.ws_v1.sock.connected and conn_timeout_v1:
            sleep(1)
            conn_timeout_v1 -= 1
        if not conn_timeout_v1:
            self.logger.error("Couldn't connect to WS-v1! Exiting.")
            self.exit_v1()
            raise websocket.WebSocketTimeoutException('Couldn\'t connect to WS-v1! Exiting.')
        
    #
    # End Public Methods
    #

    def __connect_v2(self):

        '''Connect to the websocket in a thread.'''
        self.logger.debug("Starting thread-v2")

        #websocket.enableTrace(True)
        self.ws_v2 = websocket.WebSocketApp(self.wsURL_v2,
                                         on_message=self._on_message,
                                         on_open=self.__on_open_v2,
                                         on_close=self.__on_close_v2,
                                         on_error=self.__on_error_v2)

        self.wst_v2 = threading.Thread(target=lambda: self.ws_v2.run_forever())
        self.wst_v2.daemon = True
        self.wst_v2.start()
        self.logger.info("Started thread-v2")

        # Wait for connect before continuing
        conn_timeout_v2 = 5
        while not self.ws_v2.sock or not self.ws_v2.sock.connected and conn_timeout_v2:
            sleep(1)
            conn_timeout_v2 -= 1
        if not conn_timeout_v2:
            self.logger.error("Couldn't connect to WS-v2! Exiting.")
            self.exit_v2()
            raise websocket.WebSocketTimeoutException('Couldn\'t connect to WS-v2! Exiting.')
        
    def __get_auth(self):
        '''Return auth headers. Will use API Keys if present in settings.'''
        if self.api_key:
            self.logger.info("Authenticating with API Key.")
            # To auth to the WS using an API key, we generate a signature of a nonce and
            # the WS API endpoint.
            authdata = [
                self.api_secret.encode('utf-8'),
                {
                    "op": "auth",
                    "AccessKeyId": self.api_key,
                    "SignatureMethod": "HmacSHA256",
                    "SignatureVersion": "2",
                    "Timestamp": datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S') 
                }
            ]
            # 获取需要签名的信息
            authenticaton_data = authdata[1]

            # 获取 secretkey
            _accessKeySecret = authdata[0]
            # 计算签名Signature
            authenticaton_data['Signature'] = _sign(authenticaton_data, _accessKeySecret)
            self.logger.info("Authenticaton_data: %s" %(authenticaton_data))
            return json.dumps(authenticaton_data) 
        else:
            self.logger.info("Not authenticating.")
            return []

    def __get_url(self):
        urlParts = list(urllib.parse.urlparse(self.endpoint))
        urlParts[0] = urlParts[0].replace('https', 'wss')
        urlParts[2] = "/ws/v3"
        return urllib.parse.urlunparse(urlParts)

    def __get_url_v1(self):
        urlParts = list(urllib.parse.urlparse(self.endpoint))
        urlParts[0] = urlParts[0].replace('https', 'wss')
        urlParts[2] = "/ws/v3"
        return urllib.parse.urlunparse(urlParts)

    def __get_url_v2(self):
        urlParts = list(urllib.parse.urlparse(self.endpoint))
        urlParts[0] = urlParts[0].replace('https', 'wss')
        urlParts[2] = "/ws/v3"
        return urllib.parse.urlunparse(urlParts)

    def __wait_for_account(self):
        '''On subscribe, this data will come down. Wait for it.'''
        # Wait for the keys to show up from the ws
        while not {'margin', 'position', 'order', 'orderBookL2'} <= set(self.data):
            sleep(0.1)

    def __wait_for_symbol(self, symbol):
        '''On subscribe, this data will come down. Wait for it.'''
        while not {'instrument', 'trade', 'quote'} <= set(self.data):
            sleep(0.1)

    def __send_command(self, command, args=None):
        '''Send a raw command.'''
        if args is None:
            args = []
        self.ws.send(json.dumps({"op": command, "args": args}))

    def __on_message(self, message):
        '''Handler for parsing WS messages.'''
        message = json.loads(message)
        self.logger.debug(json.dumps(message))

        table = message['table'] if 'table' in message else None
        action = message['action'] if 'action' in message else None
        try:
            if 'subscribe' in message:
                self.logger.debug("Subscribed to %s." % message['subscribe'])
            elif action:

                if table not in self.data:
                    self.data[table] = []

                # There are four possible actions from the WS:
                # 'partial' - full table image
                # 'insert'  - new row
                # 'update'  - update row
                # 'delete'  - delete row
                if action == 'partial':
                    self.logger.debug("%s: partial" % table)
                    self.data[table] += message['data']
                    # Keys are communicated on partials to let you know how to uniquely identify
                    # an item. We use it for updates.
                    self.keys[table] = message['keys']
                elif action == 'insert':
                    self.logger.debug('%s: inserting %s' % (table, message['data']))
                    self.data[table] += message['data']

                    # Limit the max length of the table to avoid excessive memory usage.
                    # Don't trim orders because we'll lose valuable state if we do.
                    if table not in ['order', 'orderBookL2'] and len(self.data[table]) > BitMEXWebsocket.MAX_TABLE_LEN:
                        self.data[table] = self.data[table][int(BitMEXWebsocket.MAX_TABLE_LEN / 2):]

                elif action == 'update':
                    self.logger.debug('%s: updating %s' % (table, message['data']))
                    # Locate the item in the collection and update it.
                    for updateData in message['data']:
                        item = findItemByKeys(self.keys[table], self.data[table], updateData)
                        if not item:
                            return  # No item found to update. Could happen before push
                        item.update(updateData)
                        # Remove cancelled / filled orders
                        if table == 'order' and item['leavesQty'] <= 0:
                            self.data[table].remove(item)
                elif action == 'delete':
                    self.logger.debug('%s: deleting %s' % (table, message['data']))
                    # Locate the item in the collection and remove it.
                    for deleteData in message['data']:
                        item = findItemByKeys(self.keys[table], self.data[table], deleteData)
                        self.data[table].remove(item)
                else:
                    raise Exception("Unknown action: %s" % action)
        except:
            self.logger.error(traceback.format_exc())

    def __on_error(self, error):
        '''Called on fatal websocket errors. We exit on these.'''
        if not self.exited:
            self.logger.error("Error : %s" % error)
            raise websocket.WebSocketException(error)

    def __on_error_v1(self, error):
        '''Called on fatal websocket errors. We exit on these.'''
        if not self.exited:
            self.logger.error("v1 Error : %s" % error)
            raise websocket.WebSocketException(error)

    def __on_error_v2(self, error):
        '''Called on fatal websocket errors. We exit on these.'''
        if not self.exited:
            self.logger.error("v2 Error : %s" % error)
            raise websocket.WebSocketException(error)

    def __on_open(self):
        '''Called when the WS opens.'''
        self.logger.info("Websocket Opened.")
        def run(*args):
            for _symbol in self.symbol:
                tradeStr = '{"op": "subscribe", "args": ["spot/trade:'+_symbol+'"]}'
                self.logger.debug("tradeStr:%s",tradeStr)
                self.ws.send(tradeStr)
        run()

    def __on_open_v1(self):
        '''Called when the WS opens.'''
        self.logger.info("Websocket Opened v1.")
        def run(*args):
            tradeStr = login_params(str(server_timestamp()), self.api_key, self.passphrase, self.api_secret)
            self.logger.debug("tradeStr:%s",tradeStr)
            self.ws_v1.send(tradeStr)
            sleep(1)

            for _symbol in self.symbol:
                tradeStr = '{"op": "subscribe", "args": ["spot/order:'+_symbol+'"]}'
                self.logger.debug("tradeStr:%s",tradeStr)
                self.ws_v1.send(tradeStr)
                _code,_currency = _symbol.split('-')
                tradeStr = '{"op": "subscribe", "args": ["spot/account:'+_code+'"]}'
                self.logger.debug("tradeStr:%s",tradeStr)
                self.ws_v1.send(tradeStr)
                
            tradeStr = '{"op": "subscribe", "args": ["spot/account:'+_currency+'"]}'
            self.logger.debug("tradeStr:%s",tradeStr)
            self.ws_v1.send(tradeStr)

            def time_func1() :
                tradeStr = 'ping'
                self.logger.debug("ping:%s",tradeStr)
                self.ws_v1.send(tradeStr)

                global timer
                timer = threading.Timer(20,time_func1)
                timer.start()
            time_func1()

        run()

    def __on_open_v2(self):
        '''Called when the WS opens.'''
        self.logger.info("Websocket Opened v2.")
        def run(*args):
            for _symbol in self.symbol:
                tradeStr = '{"op": "subscribe", "args": ["spot/depth:'+_symbol+'"]}'
                self.logger.debug("tradeStr:%s",tradeStr)
                self.ws_v2.send(tradeStr)
                tradeStr = '{"op": "subscribe", "args": ["spot/depth5:'+_symbol+'"]}'
                self.logger.debug("tradeStr:%s",tradeStr)
                self.ws_v2.send(tradeStr)
        run()

    def __on_close(self):
        '''Called on websocket close.'''
        self.logger.info('Websocket Closed')
        self.exit()
        self.__connect()
        self.logger.info('Connected to WS.')

    def __on_close_v1(self):
        '''Called on websocket close.'''
        self.logger.info('Websocket Closed v1')
        self.exit_v1()
        self.__connect_v1()
        self.logger.info('v1 Connected to WS.')

    def __on_close_v2(self):
        '''Called on websocket close.'''
        self.logger.info('Websocket Closed v2')
        self.exit_v2()
        self.__connect_v2()
        self.logger.info('v2 Connected to WS.')

# Utility method for finding an item in the store.
# When an update comes through on the websocket, we need to figure out which item in the array it is
# in order to match that item.
#
# Helpfully, on a data push (or on an HTTP hit to /api/v1/schema), we have a "keys" array. These are the
# fields we can use to uniquely identify an item. Sometimes there is more than one, so we iterate through all
# provided keys.
def findItemByKeys(keys, table, matchData):
    for item in table:
        matched = True
        for key in keys:
            if item[key] != matchData[key]:
                matched = False
        if matched:
            return item

# 计算鉴权签名
def _sign(param=None, _accessKeySecret=None):
    # 签名参数:
    params = {}
    params['SignatureMethod'] = param.get('SignatureMethod') if type(param.get('SignatureMethod')) == type(
        'a') else '' if param.get('SignatureMethod') else ''
    params['SignatureVersion'] = param.get('SignatureVersion') if type(param.get('SignatureVersion')) == type(
        'a') else '' if param.get('SignatureVersion') else ''
    params['AccessKeyId'] = param.get('AccessKeyId') if type(param.get('AccessKeyId')) == type(
        'a') else '' if param.get('AccessKeyId') else ''
    params['Timestamp'] = param.get('Timestamp') if type(param.get('Timestamp')) == type('a') else '' if param.get(
        'Timestamp') else ''
    
    # 对参数进行排序:
    keys = sorted(params.keys())
    # 加入&
    qs = '&'.join(['%s=%s' % (key, parse.quote(params[key],safe='')) for key in keys])
    # 请求方法，域名，路径，参数 后加入`\n`
    urlParts = list(urllib.parse.urlparse(MARKET_URL))
    _host = urlParts[1]
    path = '/ws/v1'
    payload = '%s\n%s\n%s\n%s' % ('GET', _host, path, qs)
    dig = hmac.new(_accessKeySecret, msg=payload.encode('utf-8'), digestmod=hashlib.sha256).digest()
    # 进行base64编码
    return base64.b64encode(dig).decode()

def get_server_time():
    url = c.API_URL + c.SERVER_TIMESTAMP_URL
    response = requests.get(url)
    if response.status_code == 200:
        return response.json()['iso']
    else:
        return ""

def server_timestamp():
    server_time = get_server_time()
    parsed_t = dp.parse(server_time)
    timestamp = parsed_t.timestamp()
    return timestamp

def login_params(timestamp, api_key, passphrase, secret_key):
    message = timestamp + 'GET' + '/users/self/verify'
    mac = hmac.new(bytes(secret_key, encoding='utf8'), bytes(message, encoding='utf-8'), digestmod='sha256')
    d = mac.digest()
    sign = base64.b64encode(d)

    login_param = {"op": "login", "args": [api_key, passphrase, timestamp, sign.decode("utf-8")]}
    login_str = json.dumps(login_param)
    return login_str

